mod capi;

use crate::try_curl;
use alloc::format;
use alloc::string::String;
use alloc::string::ToString;
use cjson::libc;
use core::ffi::{c_char, c_longlong, c_void};
use curl::easy::{Easy, WriteError};
use embed_std::sys_log::get_log_level;
use embed_std::time::now;
use embed_std::{cstr, infoln};

pub struct DownloadFile {
    pub(crate) url: String,
    pub(crate) filename: String,
}

pub struct DownloadContext {
    cb: Option<unsafe extern "C" fn(total: i64, pos: i64, arg: *mut c_void)>,
    args: *mut c_void,
}

impl DownloadFile {
    pub fn new(url: String, filename: String) -> Self {
        Self { url, filename }
    }

    pub fn start(&mut self, ctx: &DownloadContext) -> embed_std::Result<()> {
        let mut easy = create_easy(self.url.as_str())?;
        infoln!("download {} save to file {}", self.url, self.filename);
        self.filename.push_str("\0");
        unsafe {
            let f = libc::fopen(self.filename.as_ptr() as *mut c_char, cstr!("wb"));
            if f.is_null() {
                return Err(embed_std::Error::Other(format!(
                    "open file {} failed",
                    self.filename
                )));
            }
            libc::fseek(f, 0, libc::SEEK_END);
            let pos = libc::ftell(f);
            libc::fseek(f, 0, libc::SEEK_SET);
            let mut handle = || -> embed_std::Result<()> {
                try_curl!(easy.get(true))?;
                try_curl!(easy.follow_location(true))?;
                try_curl!(easy.progress(true))?;
                if pos > 0 {
                    infoln!("resume from {}", pos);
                    try_curl!(easy.resume_from(pos as u64))?;
                }
                let f_p = f as usize;
                try_curl!(easy.write_function(move |data| {
                    let ret = libc::fwrite(
                        data.as_ptr() as *const c_void,
                        1,
                        data.len() as libc::size_t,
                        f_p as *mut libc::FILE,
                    );
                    if ret >= data.len() as libc::size_t {
                        Ok(ret as usize)
                    } else {
                        Err(WriteError::Pause)
                    }
                }))?;
                let p = ctx as *const DownloadContext as usize;
                try_curl!(easy.progress_function(move |a1, a2, _a3, _a4| {
                    let c = &mut *(p as *const DownloadContext as *mut DownloadContext);
                    if let Some(cb) = c.cb {
                        cb(a1 as c_longlong, a2 as c_longlong, c.args);
                    }
                    true
                }))?;
                try_curl!(easy.perform())?;
                Ok(())
            };
            handle()?;
            if !f.is_null() {
                libc::fclose(f);
            }
        }
        Ok(())
    }
}

pub const CERT_PERM_FILE_NAME: &str = "cacert.pem";

pub fn create_easy(url: &str) -> embed_std::Result<Easy> {
    let mut client = Easy::new();
    if get_log_level() == 1 {
        try_curl!(client.verbose(true))?;
    }
    try_curl!(client.url(url))?;
    #[cfg(not(target_arch = "x86_64"))]
    {
        use embed_std::debugln;
        use embed_std::util::cstr_to_str;
        let v = unsafe { libc::getenv("CURL_CA_BUNDLE\0".as_ptr() as *const c_char) };
        let cert_file = if !v.is_null() {
            cstr_to_str(v)
        } else {
            CERT_PERM_FILE_NAME
        };
        debugln!("cert file: {}", cert_file);
        try_curl!(client.cainfo(cert_file))?;
    }
    Ok(client)
}

pub fn calc_d_access_token(device_uid: &str, secret: &str) -> String {
    let ts = now().as_secs().to_string();
    let txt = format!("{}:{}:{}", device_uid, secret, ts);
    let mut data = String::new();
    data.push_str(format!("{}$", device_uid).as_str());
    let digest = openssl::sha::sha1(txt.as_bytes());
    for i in 0..digest.len() {
        data.push_str(format!("{:02x}", digest[i]).as_str());
    }
    data.push_str(format!("${}", ts).as_str());
    data
}
